// Copyright 2020 The Tilt Brush Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using UnityEngine;

namespace TiltBrush
{

    class SquareBrush : GeometryBrush
    {
        protected const int BBR_B = 0; // back bottom right
        protected const int BBR_R = 1; // back bottom right
        protected const int BTL_T = 2; // back top left
        protected const int BTL_L = 3; // back top left
        protected const int BTR_T = 4; // back top right
        protected const int BTR_R = 5; // back top right
        protected const int BBL_B = 6; // back bottom left
        protected const int BBL_L = 7; // back bottom left
        protected const int FBR_B = 8; // etc
        protected const int FBR_R = 9; // etc
        protected const int FTL_T = 10;
        protected const int FTL_L = 11;
        protected const int FTR_T = 12;
        protected const int FTR_R = 13;
        protected const int FBL_B = 14;
        protected const int FBL_L = 15;

        const float kSolidMinLengthMeters_PS = 0.002f;
        const float kMinMoveLengthMeters_PS = 5e-4f;
        const float kBreakAngleScalar = 2.0f;
        const float kSolidAspectRatio = 0.2f;
        const float kCrossSectionAspectRatio = .375f; // height / width

        public SquareBrush()
            : base(bCanBatch: true,
                upperBoundVertsPerKnot: 8,
                bDoubleSided: false)
        {
        }

        //
        // GeometryBrush API
        //

        protected override void InitBrush(BrushDescriptor desc,
                                          TrTransform localPointerXf)
        {
            base.InitBrush(desc, localPointerXf);
            m_geometry.Layout = GetVertexLayout(desc);
        }

        override public GeometryPool.VertexLayout GetVertexLayout(BrushDescriptor desc)
        {
            return new GeometryPool.VertexLayout
            {
                // Don't use UVs, but don't want to write another shader that doesn't sample
                uv0Size = 2,
                bUseNormals = true,
                bUseColors = true
            };
        }

        override public float GetSpawnInterval(float pressure01)
        {
            return kSolidMinLengthMeters_PS * POINTER_TO_LOCAL * App.METERS_TO_UNITS +
                (PressuredSize(pressure01) * kSolidAspectRatio);
        }

        override protected void ControlPointsChanged(int iKnot0)
        {
            // Frames knots, determines how much geometry each knot should get
            OnChanged_FrameKnots(iKnot0);
            ResizeGeometry();

            // Updating a control point affects geometry generated by previous knot
            // (if there is any). The HasGeometry check is not a micro-optimization:
            // it also keeps us from backing up past knot 0.
            {
                int start = (m_knots[iKnot0 - 1].HasGeometry) ? iKnot0 - 1 : iKnot0;
                OnChanged_MakeGeometry(start);
            }
        }

        // Fills in any knot data needed for geometry generation. Updates:
        // - length, nRight, nSurface
        // - HasGeometry, startsGeometry, endsGeometry
        // - iVert, nVert, iTri, nTri
        void OnChanged_FrameKnots(int iKnot0)
        {
            float minMove = kMinMoveLengthMeters_PS * App.METERS_TO_UNITS * POINTER_TO_LOCAL;

            Knot prev = m_knots[iKnot0 - 1];
            for (int iKnot = iKnot0; iKnot < m_knots.Count; ++iKnot)
            {
                Knot cur = m_knots[iKnot];

                bool shouldBreak = false;

                Vector3 vMove = cur.point.m_Pos - prev.point.m_Pos;
                cur.length = vMove.magnitude;

                // Rather than use unstable math, just bail and don't change the geometry.
                if (cur.length < minMove)
                {
                    // nb. it's only safe to bail if we haven't already started changing state
                    // (thus the assert that this is the last knot). We also only expect this to
                    // happen for the dynamic leading edge (thus the assert that this is the first
                    // knot in the update list).
                    shouldBreak = true;
                }

                // invariant: nSurface = nMove x nRight
                // If single-sided, always point the frontside towards the brush. Causes twisting.
                Vector3 nMove = vMove / cur.length;
                Vector3 vPreferredRight = m_Desc.m_BackIsInvisible
                    ? Vector3.Cross(cur.point.m_Orient * Vector3.forward, nMove)
                    : prev.nRight;
                ComputeSurfaceFrameNew(
                    vPreferredRight, nMove, cur.point.m_Orient,
                    out cur.nRight, out cur.nSurface);

                // More break checking; replicates previous logic
                if (prev.HasGeometry)
                {
                    float fWidthHeightRatio = cur.length / PressuredSize(cur.smoothedPressure);
                    float fBreakAngle = Mathf.Atan(fWidthHeightRatio) * Mathf.Rad2Deg * kBreakAngleScalar;
                    Vector3 vPrevMove = prev.point.m_Pos - m_knots[iKnot - 2].point.m_Pos;
                    if (Vector3.Angle(vPrevMove, vMove) > fBreakAngle)
                    {
                        shouldBreak = true;
                    }
                }

                if (shouldBreak)
                {
                    // These things are documented as being invalid unless HasGeometry,
                    // so technically we don't need to null them out
                    cur.nRight = cur.nSurface = Vector3.zero;
                    cur.startsGeometry = cur.endsGeometry = false;

                    // nb: duplicated down below too
                    if (prev.HasGeometry && !prev.endsGeometry)
                    {
                        prev.endsGeometry = true;
                        prev.nTri += 2;
                        m_knots[iKnot - 1] = prev;
                    }

                    cur.iTri = prev.iTri + prev.nTri;
                    cur.iVert = (ushort)(prev.iVert + prev.nVert);
                    cur.nTri = cur.nVert = (ushort)0;
                }
                else
                {
                    if (prev.HasGeometry && prev.endsGeometry)
                    {
                        prev.endsGeometry = false;
                        prev.nTri -= 2;
                        m_knots[iKnot - 1] = prev;
                    }

                    cur.startsGeometry = !prev.HasGeometry;
                    cur.endsGeometry = false; // this will be fixed up next iteration
                    cur.nVert = 16;
                    cur.nTri = 8;
                    cur.iTri = prev.iTri + prev.nTri;
                    if (cur.startsGeometry)
                    {
                        // no shared verts with previous.
                        // 2 extra triangles for start cap.
                        cur.iVert = (ushort)(prev.iVert + prev.nVert);
                        cur.nTri += 2;
                    }
                    else
                    {
                        // 8 shared verts with previous.
                        cur.iVert = (ushort)(prev.iVert + prev.nVert - 8);
                    }
                }

                m_knots[iKnot] = cur;
                prev = cur;
            }

            // nb: copied from the loop
            if (prev.HasGeometry && !prev.endsGeometry)
            {
                prev.endsGeometry = true;
                prev.nTri += 2;
                m_knots[m_knots.Count - 1] = prev;
            }
        }

        // Create verts, tris
        void OnChanged_MakeGeometry(int iKnot0)
        {

            // Invariant: there is a previous knot.
            Knot prev = m_knots[iKnot0 - 1];

            for (int iKnot = iKnot0; iKnot < m_knots.Count; ++iKnot)
            {
                // Invariant: all of prev's geometry (if any) is correct and up-to-date.
                // Thus, there is no need to modify anything shared with prev.
                Knot cur = m_knots[iKnot];

                if (cur.HasGeometry)
                {
                    int nTri = 0;

                    // Top quads
                    SetTri(cur.iTri, cur.iVert, nTri++, BTR_T, BTL_T, FTR_T);
                    SetTri(cur.iTri, cur.iVert, nTri++, FTR_T, BTL_T, FTL_T);
                    // Right quads
                    SetTri(cur.iTri, cur.iVert, nTri++, BTR_R, FTR_R, BBR_R);
                    SetTri(cur.iTri, cur.iVert, nTri++, BBR_R, FTR_R, FBR_R);
                    // Left quads
                    SetTri(cur.iTri, cur.iVert, nTri++, BBL_L, FBL_L, BTL_L);
                    SetTri(cur.iTri, cur.iVert, nTri++, BTL_L, FBL_L, FTL_L);
                    // Bottom quads
                    SetTri(cur.iTri, cur.iVert, nTri++, BBL_B, BBR_B, FBL_B);
                    SetTri(cur.iTri, cur.iVert, nTri++, FBL_B, BBR_B, FBR_B);

                    // Endcaps
                    if (cur.startsGeometry)
                    {
                        SetTri(cur.iTri, cur.iVert, nTri++, BTR_R, BBR_R, BTL_L);
                        SetTri(cur.iTri, cur.iVert, nTri++, BTL_L, BBR_R, BBL_L);
                    }
                    if (cur.endsGeometry)
                    {
                        SetTri(cur.iTri, cur.iVert, nTri++, FTR_R, FTL_L, FBR_R);
                        SetTri(cur.iTri, cur.iVert, nTri++, FBR_R, FTL_L, FBL_L);
                    }
                    Debug.Assert(nTri == cur.nTri);

                    if (cur.startsGeometry)
                    {
                        // Can't use prev.nRight, prev.nSurface; they're invalid if no geometry
                        float size = PressuredSize(prev.smoothedPressure);
                        Vector3 halfR = cur.nRight * (size / 2);
                        Vector3 halfU = cur.nSurface * ((size / 2) * kCrossSectionAspectRatio);
                        MySetVert(cur.iVert, BBR_B, prev.point.m_Pos - halfU + halfR, -cur.nSurface);
                        MySetVert(cur.iVert, BBR_R, prev.point.m_Pos - halfU + halfR, cur.nRight);
                        MySetVert(cur.iVert, BTL_T, prev.point.m_Pos + halfU - halfR, cur.nSurface);
                        MySetVert(cur.iVert, BTL_L, prev.point.m_Pos + halfU - halfR, -cur.nRight);
                        MySetVert(cur.iVert, BTR_T, prev.point.m_Pos + halfU + halfR, cur.nSurface);
                        MySetVert(cur.iVert, BTR_R, prev.point.m_Pos + halfU + halfR, cur.nRight);
                        MySetVert(cur.iVert, BBL_B, prev.point.m_Pos - halfU - halfR, -cur.nSurface);
                        MySetVert(cur.iVert, BBL_L, prev.point.m_Pos - halfU - halfR, -cur.nRight);
                    }

                    {
                        float size = PressuredSize(cur.smoothedPressure);
                        Vector3 halfR = cur.nRight * (size / 2);
                        Vector3 halfU = cur.nSurface * ((size / 2) * kCrossSectionAspectRatio);
                        MySetVert(cur.iVert, FBR_B, cur.point.m_Pos - halfU + halfR, -cur.nSurface);
                        MySetVert(cur.iVert, FBR_R, cur.point.m_Pos - halfU + halfR, cur.nRight);
                        MySetVert(cur.iVert, FTL_T, cur.point.m_Pos + halfU - halfR, cur.nSurface);
                        MySetVert(cur.iVert, FTL_L, cur.point.m_Pos + halfU - halfR, -cur.nRight);
                        MySetVert(cur.iVert, FTR_T, cur.point.m_Pos + halfU + halfR, cur.nSurface);
                        MySetVert(cur.iVert, FTR_R, cur.point.m_Pos + halfU + halfR, cur.nRight);
                        MySetVert(cur.iVert, FBL_B, cur.point.m_Pos - halfU - halfR, -cur.nSurface);
                        MySetVert(cur.iVert, FBL_L, cur.point.m_Pos - halfU - halfR, -cur.nRight);
                    }
                }

                prev = cur;
            }
        }

        void MySetVert(int iVert, int vp, Vector3 v, Vector3 n)
        {
            int i = iVert + vp * NS;
            m_geometry.m_Vertices[i] = v;
            m_geometry.m_Normals[i] = n;
            Color32 c = m_Color;
            c.a = 255;
            m_geometry.m_Colors[i] = c;
            m_geometry.m_Texcoord0.v2[i] = new Vector2(.5f, .5f);
        }

    }
} // namespace TiltBrush
